package ai.neuralnet;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Random;


public abstract class TNeuron<N extends TNeuron>
{

    private double lastOutput;
    private boolean isInputNeuron;
    int inputCount = 0;
    protected double biasInput = 1;
    protected double[] weights;
    protected double[] inputs;
    private boolean outputInvalid = true;
    protected HashMap<N, Integer> inputNeurons;
    protected boolean[] inputSet;
    protected ArrayList<N> outputs;


    public TNeuron(int inputCount)
    {
        weights = new double[inputCount + 1];
        inputNeurons = new HashMap<N, Integer>(inputCount);
        outputs = new ArrayList<N>();
        inputSet = new boolean[inputCount];
        inputs = new double[inputCount];
    }


    public void tryPropagateForward()
    {

        if (!allInputsReady())
            return;

        double output = computeOutput();

        for (TNeuron n : outputs)
        {
            int index = n.getInputIndex(this);
            n.setInput(index, output);
        }

        resetInputSet();
    }

    public void resetInputSet()
    {
        for (int i = 0; i < inputSet.length; i++)
        {
            inputSet[i] = false;
        }
    }

    public boolean allInputsReady()
    {
        boolean ready = true;
        for (int i = 0; i < inputSet.length; i++)
        {
            ready &= inputSet[i];
            if (!ready) break;
        }
        return ready;
    }

    public void addInputNeuron(N n)
    {
        if (inputCount < inputs.length)
            inputNeurons.put(n, inputCount++);
        else
            System.err.println("Error: Cannot add more neurons inputCount than neuron size");
    }

    public void addInputNeuron(Collection<N> ns)
    {
        for (N n : ns)
        {
            if (inputCount < inputs.length)
                inputNeurons.put(n, inputCount++);
            else
                System.err.println("Error: Cannot add more neurons inputCount than neuron size");
        }
    }

    protected int getInputIndex(N n)
    {
        return inputNeurons.get(n);
    }

    protected double computeInputSum()
    {
        double sum = weights[0] * biasInput;

        for (int i = 0; i < inputs.length; i++)
        {
            sum += weights[i + 1] * inputs[i];
        }

        return sum;

    }

    public void setInput(int index, double value)
    {
        if (inputSet[index] == true)
        {
            System.out.println("Overwriting input ... probable bug");
        }

        inputs[index] = value;
        inputSet[index] = true;

        outputInvalid = true;
    }

    public double computeOutput()
    {
        if (isInputNeuron) return inputs[0];
        //if new propagation has happened since last call to compute output the recompute value
        if (outputInvalid)
        {
            double sum = computeInputSum();
            lastOutput = 1. / (1 + Math.exp(-sum));
            outputInvalid = false;
        }
        //return most recent output
        return lastOutput;
    }

    public void setIsInputNeuron(boolean inputNeuron)
    {
        isInputNeuron = inputNeuron;
    }

    public void addOutputNeuron(Collection<N> neurons)
    {
        outputs.addAll(neurons);
    }

    public void addOutputNeuron(N neuron)
    {
        outputs.add(neuron);
    }

    public void randomizeWeights(double minValue, double maxValue)
    {
        Random r = new Random();
        for (int i = 0; i < weights.length; i++)
        {
            weights[i] = minValue + r.nextDouble() * (maxValue - minValue);
        }

        outputInvalid = true;
    }

    public double[] getWeights()
    {
        return weights;
    }

    public void setWeights(double weight)
    {
        for (int i = 0; i < weights.length; i++)
        {
            weights[i] = weight;
        }
        outputInvalid = true;
    }

    public HashMap<N, Integer> getInputNeurons()
    {
        return inputNeurons;
    }

    public void setWeights(double[] weights)
    {
        for (int i = 0; i < this.weights.length; i++)
        {
            this.weights[i] = weights[i];
        }

        outputInvalid = true;
    }
}
